﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using NTypeClasses;
using System.IO;
using Mono.Cecil;

namespace TypeClassCheck
{
	class TypeClassCheckProgram
	{
		static AssemblyDefinition adef;
		static bool verbose = false;

		static int Main(string[] args)
		{
			if (args.Length != 1) {
				return Usage();
			}

			
			try {
				args[0] = Path.GetFullPath(args[0]);
				Assembly.ReflectionOnlyLoadFrom(args[0]);
				adef = AssemblyDefinition.ReadAssembly(args[0]);
			} catch (FileNotFoundException) {
				Console.Error.WriteLine("file {0} was not found", args[0]);
				return -2;
			} catch (ArgumentException e) {
				Console.Error.WriteLine(e.Message);
				return -3;
			}

			int errors = 0;

			foreach (var typedef in adef.Modules.SelectMany(m => m.Types)){
					var methods = typedef.Methods.Where(method => method.DeclaringType == typedef).ToArray();
					var paramTypes = methods.SelectMany(method => method.Parameters.Select(param => param.ParameterType)).ToArray();
					var variableTypes = methods.SelectMany(method => method.Body == null 
						? Enumerable.Empty<TypeReference>()
						: method.Body.Variables.Select(v => v.VariableType)).ToArray();
					var alltypes = paramTypes.Concat(variableTypes)
						.Select(GetReflectionType).Distinct().Where(t => t != null);
					var usingTypeClasses = alltypes.Where(type => type.Assembly.DependsOn(TypeClasses.Assembly));
					var candidates = usingTypeClasses.SelectMany(t => t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly))
						.Select(f => f.FieldType);
					errors += candidates.Sum((Func<Type, int>)Check);
			}

			return errors > 0? 1: 0;
		}

		static readonly HashSet<int> checkDone = new HashSet<int>();
		static readonly HashSet<int> checkedTypes = new HashSet<int>();
		static readonly Dictionary<string, Type> cachedTypes = new Dictionary<string, Type>();

		static Type GetReflectionType(TypeReference reference)
		{
			Type result;
			if (cachedTypes.TryGetValue(reference.FullName, out result)) return result;
			var genericType = reference as GenericInstanceType;
			if (genericType == null || genericType.GenericArguments.Any(arg => arg.IsGenericParameter)) return null;
			var typedef = genericType.Resolve();
			foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Concat(AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies())) {
				if (assembly != TypeClasses.Assembly && !assembly.DependsOn(TypeClasses.Assembly)) continue;
				var type = assembly.GetTypes().FirstOrDefault(t => t.FullName == typedef.FullName);
				if (type != null) {
					type = type.MakeGenericType(genericType.GenericArguments.Select(t => Type.GetType(t.FullName, true)).ToArray());
					cachedTypes[reference.FullName] = type;
					return type;
				}
			}

			cachedTypes[reference.FullName] = null;
			return null;
		}

		static int Check(Type type)
		{
			if (!type.IsAbstract || checkedTypes.Contains(type.MetadataToken)) return 0;

			Console.WriteLine("checking {0}...", type);
			int errorCount = 0;
			foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Public)) {
				if (!method.IsAbstract || !checkDone.Add(method.MetadataToken)) continue;
				var impl = TypeClasses.GetMethodImplementation(type, method);

				if (impl == null) {
					errorCount++;
					Console.Error.WriteLine("{0}: no implementation for {1}", BeautyfulTypeName(type), method);
				} else {
					if (verbose) Console.WriteLine("{0}: implemented {1}", type.Name, method);
				}
			}
			return errorCount;
		}

		static string StripGenericParameterCount(string typeName)
		{
			int start = typeName.IndexOf('`');
			return start >= 0 ? typeName.Substring(0, start) : typeName;
		}

		static string BeautyfulTypeName(Type typeClass)
		{
			var result = new StringBuilder(StripGenericParameterCount(typeClass.Name));
			if (typeClass.GetGenericArguments().Length == 0) return result.ToString();
			result.Append('<');
			bool first = true;
			foreach (var param in typeClass.GetGenericArguments()) {
				var name = BeautyfulTypeName(param);
				if (!first) result.Append(", ");
				first = false;
				result.Append(name);
			}
			result.Append('>');
			return result.ToString();
		}

		static int Usage()
		{
			Console.WriteLine("usage: TypeClassCheck assemblyPath");
			return -1;
		}
	}
}
